package com.sinovatech.ologger.core.web.jsonplugin;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.opensymphony.webwork.WebWorkStatics;
import com.opensymphony.xwork.ActionContext;
import com.opensymphony.xwork.ActionInvocation;
import com.opensymphony.xwork.Result;
import com.opensymphony.xwork.util.OgnlValueStack;

/**
 * <!-- START SNIPPET: description -->
 *
 * This result serializes an action into JSON.
 *
 * <!-- END SNIPPET: description -->
 *
 * <p/> <u>Result parameters:</u>
 *
 * <!-- START SNIPPET: parameters -->
 *
 * <ul>
 * <li><b>excludeProperties</b> - list of regular expressions matching the properties to be excluded.
 * The regular expressions are evaluated against the OGNL expression representation of the properties. </li>
 *  <li><b>defaultEncoding</b> - Response & json encoding, default UTF-8</li>
 *  <li><b>enableGZIP</b> - zip Response, default false</li>
 *  <li><b>ignoreHierarchy</b> - Instrospect bean and serialize its superclass, default true</li>
 *  <li><b>maxFetchLevel</b> - Fetch and serialize object level. default 6</li>
 *  <li><b>root</b> - OGNL expression of root object to be serialized.</li>
 *  <li><b>noCache</b> - Add headers to response to prevent the browser from caching the response.</li>
 * </ul>
 *
 * <!-- END SNIPPET: parameters -->
 *
 * <b>Example:</b>
 *
 * <pre><!-- START SNIPPET: example -->
 * &lt;result name="success" type="json" /&gt;
 * <!-- END SNIPPET: example --></pre>
 *
 */
public class JSONResult implements Result {
	private static final long serialVersionUID = 8624350183189931165L;
	private static final Log log = LogFactory.getLog(JSONResult.class);
	private String defaultEncoding = "UTF-8";
	private List<Pattern> includeProperties;
	private List<Pattern> excludeProperties;
	private String root;
	private boolean wrapWithComments;
	private boolean enableGZIP = false;
	private boolean ignoreHierarchy = true;
	private boolean enumAsBean = JSONWriter.ENUM_AS_BEAN_DEFAULT;
	private boolean noCache = true;
	private boolean excludeNullProperties = false;
	private int maxFetchLevel = 6;

	public void setMaxFetchLevel(int maxFetchLevel) {
		this.maxFetchLevel = maxFetchLevel;
	}

	public void setDefaultEncoding(String val) {
		defaultEncoding = val;
	}

	/**
	 * Gets a list of regular expressions of properties to exclude
	 * from the JSON output.
	 *
	 * @return A list of compiled regular expression patterns
	 */
	public List<Pattern> getExcludePropertiesList() {
		return excludeProperties;
	}

	/**
	 * Sets a comma-delimited list of regular expressions to match
	 * properties that should be excluded from the JSON output.
	 *
	 * @param commaDelim A comma-delimited list of regular expressions
	 */
	public void setExcludeProperties(String commaDelim) {
		List<String> excludePatterns = JSONUtil.asList(commaDelim);
		if (excludePatterns != null) {
			excludeProperties = new ArrayList<Pattern>(excludePatterns.size());
			for (String pattern : excludePatterns) {
				excludeProperties.add(Pattern.compile(pattern));
			}
		}
	}

	/**
	 * @return the includeProperties
	 */
	public List<Pattern> getIncludePropertiesList() {
		return includeProperties;
	}

	/**
	 * @param includedProperties the includeProperties to set
	 */
	public void setIncludeProperties(String commaDelim) {
		List<String> includePatterns = JSONUtil.asList(commaDelim);
		if (includePatterns != null) {
			includeProperties = new ArrayList<Pattern>(includePatterns.size());

			HashMap<String, String> existingPatterns = new HashMap<String, String>();

			for (String pattern : includePatterns) {
				// Compile a pattern for each *unique* "level" of the object
				// hierarchy specified in the regex.
				String[] patternPieces = pattern.split("\\\\\\.");

				String patternExpr = "";
				for (String patternPiece : patternPieces) {
					if (patternExpr.length() > 0) {
						patternExpr += "\\.";
					}
					patternExpr += patternPiece;

					// Check for duplicate patterns so that there is no overlap.
					if (!existingPatterns.containsKey(patternExpr)) {
						existingPatterns.put(patternExpr, patternExpr);

						// Add a pattern that does not have the indexed property matching (ie. list\[\d+\] becomes list).
						if (patternPiece.endsWith("\\]")) {
							includeProperties.add(Pattern.compile(patternExpr.substring(0, patternPiece
									.lastIndexOf("\\["))));

							if (log.isDebugEnabled()) {
								log.debug("Adding include property expression:  "
										+ patternExpr.substring(0, patternPiece.lastIndexOf("\\[")));
							}
						}

						includeProperties.add(Pattern.compile(patternExpr));

						if (log.isDebugEnabled()) {
							log.debug("Adding include property expression:  " + patternExpr);
						}
					}
				}
			}
		}
	}

	public void execute(ActionInvocation invocation) throws Exception {
		ActionContext actionContext = invocation.getInvocationContext();
		HttpServletRequest request = (HttpServletRequest) actionContext.get(WebWorkStatics.HTTP_REQUEST);
		HttpServletResponse response = (HttpServletResponse) actionContext.get(WebWorkStatics.HTTP_RESPONSE);

		try {
			String json;
			Object rootObject;
			// generate JSON
			if (root != null) {
				OgnlValueStack stack = invocation.getStack();
				rootObject = stack.findValue(root);
			} else {
				rootObject = invocation.getAction();
			}
			json = JSONUtil.serialize(rootObject, excludeProperties, includeProperties, ignoreHierarchy, enumAsBean,
					excludeNullProperties, maxFetchLevel);
			boolean writeGzip = enableGZIP && JSONUtil.isGzipInRequest(request);
			writeToResponse(response, json, writeGzip);
		} catch (IOException exception) {
			log.error(exception.getMessage(), exception);
			throw exception;
		}
	}

	protected void writeToResponse(HttpServletResponse response, String json, boolean gzip) throws IOException {
		JSONUtil.writeJSONToResponse(response, getEncoding(), isWrapWithComments(), json, false, gzip, noCache);
	}

	/**
	 * Retrieve the encoding
	 * <p/>
	 *
	 * @return The encoding associated with this template (defaults to the value of 'struts.i18n.encoding' property)
	 */
	protected String getEncoding() {
		String encoding = defaultEncoding;

		if (encoding == null) {
			encoding = System.getProperty("file.encoding");
		}

		if (encoding == null) {
			encoding = "UTF-8";
		}

		return encoding;
	}

	/**
	 * @return  OGNL expression of root object to be serialized
	 */
	public String getRoot() {
		return root;
	}

	/**
	 * Sets the root object to be serialized, defaults to the Action
	 *
	 * @param root OGNL expression of root object to be serialized
	 */
	public void setRoot(String root) {
		this.root = root;
	}

	/**
	 * @return Generated JSON must be enclosed in comments
	 */
	public boolean isWrapWithComments() {
		return wrapWithComments;
	}

	/**
	 * Wrap generated JSON with comments
	 * @param wrapWithComments
	 */
	public void setWrapWithComments(boolean wrapWithComments) {
		this.wrapWithComments = wrapWithComments;
	}

	public void setIgnoreHierarchy(boolean ignoreHierarchy) {
		this.ignoreHierarchy = ignoreHierarchy;
	}

	/**
	 * Controls how Enum's are serialized :
	 *    If true, an Enum is serialized as a name=value pair (name=name()) (default)
	 *    If false, an Enum is serialized as a bean with a special property _name=name()
	 *
	 * @param enumAsBean
	 */
	public void setEnumAsBean(boolean enumAsBean) {
		this.enumAsBean = enumAsBean;
	}

	public boolean isEnumAsBean() {
		return enumAsBean;
	}

	public boolean isEnableGZIP() {
		return enableGZIP;
	}

	public void setEnableGZIP(boolean enableGZIP) {
		this.enableGZIP = enableGZIP;
	}

	public boolean isNoCache() {
		return noCache;
	}

	/**
	 * Add headers to response to prevent the browser from caching the response
	 * @param noCache
	 */
	public void setNoCache(boolean noCache) {
		this.noCache = noCache;
	}

	public boolean isIgnoreHierarchy() {
		return ignoreHierarchy;
	}

	public boolean isExcludeNullProperties() {
		return excludeNullProperties;
	}

	/**
	 * Do not serialize properties with a null value
	 * @param excludeNullProperties
	 */
	public void setExcludeNullProperties(boolean excludeNullProperties) {
		this.excludeNullProperties = excludeNullProperties;
	}
}
